##########################################################################

# MODFIND.TCL, available or loaded module find procedures
# Copyright (C) 2002-2004 Mark Lakata
# Copyright (C) 2004-2017 Kent Mein
# Copyright (C) 2016-2021 Xavier Delaruelle
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

##########################################################################

# convert environment variable references in string to their values
# every local variable is prefixed by '0' to ensure they will not be
# overwritten through variable reference resolution process
proc resolvStringWithEnv {0str} {
   # fetch variable references in string
   set 0match_list [regexp -all -inline {\$[{]?([A-Za-z_][A-Za-z0-9_]*)[}]?}\
      ${0str}]
   if {[llength ${0match_list}] > 0} {
      # put in local scope every environment variable referred in string
      for {set 0i 1} {${0i} < [llength ${0match_list}]} {incr 0i 2} {
         set 0varname [lindex ${0match_list} ${0i}]
         if {![info exists ${0varname}]} {
            set ${0varname} [get-env ${0varname}]
         }
      }
      # resolve variable reference with values (now in local scope)
      set 0res [subst -nobackslashes -nocommands ${0str}]
   } else {
      set 0res ${0str}
   }

   reportDebug "'${0str}' resolved to '${0res}'"

   return ${0res}
}

# deduce modulepath from modulefile and module name
proc getModulepathFromModuleName {modfile modname} {
   return [string range $modfile 0 end-[string length /$modname]]
}

# deduce module name from modulefile and modulepath
proc getModuleNameFromModulepath {modfile modpath} {
   return [string range $modfile [string length $modpath/] end]
}

# extract module name from modulefile and currently enabled modulepaths
proc findModuleNameFromModulefile {modfile} {
   set ret {}

   foreach modpath [getModulePathList] {
      if {[string first $modpath/ $modfile/] == 0} {
         set ret [getModuleNameFromModulepath $modfile $modpath]
         break
      }
   }
   return $ret
}

# extract modulepath from modulefile and currently enabled modulepaths
proc findModulepathFromModulefile {modfile} {
   set ret {}

   foreach modpath [getModulePathList] {
      if {[string first $modpath/ $modfile/] == 0} {
         set ret $modpath
         break
      }
   }
   return $ret
}

# Determine with a name provided as argument the corresponding module name,
# version and name/version. Module name is guessed from current module name
# when shorthand version notation is used. Both name and version are guessed
# from current module if name provided is empty. If 'name_relative_tocur' is
# enabled then name argument may be interpreted as a name relative to the
# current modulefile directory (useful for module-version and module-alias
# for instance).
proc getModuleNameVersion {{name {}} {name_relative_tocur 0}} {
   set curmod [currentState modulename]
   set curmodname [file dirname $curmod]
   set curmodversion [file tail $curmod]

   if {$name eq {}} {
      set name $curmodname
      set version $curmodversion
   # check for shorthand version notation like "/version" or "./version"
   # only if we are currently interpreting a modulefile or modulerc
   } elseif {$curmod ne {} && [regexp {^\.?\/(.*)$} $name match version]} {
      # if we cannot distinguish a module name, raise error when shorthand
      # version notation is used
      if {[currentState modulefile] ne $curmod && $curmod ne {.modulerc}} {
         # name is the name of current module directory
         set name $curmodname
      } else {
         reportError "Invalid modulename '$name' found"
         return {}
      }
   } else {
      set name [string trimright $name /]
      set version [file tail $name]
      if {$name eq $version} {
         set version {}
      } else {
         set name [file dirname $name]
      }
      # name may correspond to last part of current module
      # if so name is replaced by current module name
      if {$name_relative_tocur && [file tail $curmodname] eq $name} {
         set name $curmodname
      }
   }

   if {$version eq {}} {
      set mod $name
   } else {
      set mod $name/$version
   }

   return [list $mod $name $version]
}

# Register alias or symbolic version deep resolution in a global array that
# can be used thereafter to get in one query the actual modulefile behind
# a virtual name. Also consolidate a global array that in the same manner
# list all the symbols held by modulefiles.
proc setModuleResolution {mod target {symver {}} {override_res_path 1}\
   {auto_symver 0}} {
   global g_moduleResolved g_resolvedHash g_resolvedPath g_symbolHash

   # find end-point module and register step-by-step path to get to it
   set res $target
   lappend res_path $res
   while {$mod ne $res && [info exists g_resolvedPath($res)]} {
      set res $g_resolvedPath($res)
      lappend res_path $res
   }

   # error if resolution end on initial module
   if {$mod eq $res} {
      reportError "Resolution loop on '$res' detected"
      return 0
   }

   # module name will be useful when registering symbol
   if {$symver ne {}} {
      lassign [getModuleNameVersion $mod] modfull modname
   }

   # change default symbol owner if previously given; auto symbol are defined
   # only if no default is pre-existing
   if {$symver eq {default} && !$auto_symver} {
      # alternative name "modname" is set when mod = "modname/default" both
      # names will be registered to be known for queries and resolution defs
      set modalt $modname

      if {[info exists g_moduleResolved($mod)]} {
         set prev $g_moduleResolved($mod)
         # there may not be a default in case of auto symbol
         if {[info exists g_symbolHash($prev)] && [set idx [lsearch -exact\
            $g_symbolHash($prev) default]] != -1} {
            reportDebug "remove symbol 'default' from '$prev'"
            set g_symbolHash($prev) [lreplace $g_symbolHash($prev) $idx $idx]
         }
      }
   }

   # register end-point resolution
   reportDebug "$mod resolved to $res"
   set g_moduleResolved($mod) $res
   # set first element of resolution path only if not already set or
   # scratching enabled, no change when propagating symbol along res path
   if {$override_res_path || ![info exists g_resolvedPath($mod)]} {
      set g_resolvedPath($mod) $target
   }
   lappend g_resolvedHash($res) $mod

   # also register resolution on alternative name if any
   if {[info exists modalt]} {
      reportDebug "$modalt resolved to $res"
      set g_moduleResolved($modalt) $res
      if {$override_res_path || ![info exists g_resolvedPath($modalt)]} {
         set g_resolvedPath($modalt) $target
      }
      lappend g_resolvedHash($res) $modalt
      # register name alternative to know their existence
      set ::g_moduleAltName($modalt) $mod
      set ::g_moduleAltName($mod) $modalt
   }

   # if other modules were pointing to this one, adapt resolution end-point
   set relmod_list {}
   if {[info exists g_resolvedHash($mod)]} {
      set relmod_list $g_resolvedHash($mod)
      unset g_resolvedHash($mod)
   }
   # also adapt resolution for modules pointing to the alternative name
   if {[info exists modalt] && [info exists g_resolvedHash($modalt)]} {
      lappend relmod_list {*}$g_resolvedHash($modalt)
      unset g_resolvedHash($modalt)
   }
   foreach relmod $relmod_list {
      set g_moduleResolved($relmod) $res
      reportDebug "$relmod now resolved to $res"
      lappend g_resolvedHash($res) $relmod
   }

   # register and propagate symbols to the resolution path, exception made for
   # auto symbol which are stored separately and not propagated
   if {[info exists g_symbolHash($mod)]} {
      set sym_list $g_symbolHash($mod)
   } else {
      set sym_list {}
   }
   if {$symver ne {} && $auto_symver} {
      reportDebug "define auto symbolic version '$mod' targeting $target"
      set ::g_autoSymbol($mod) $target
   } elseif {$symver ne {} && !$auto_symver} {
      # merge symbol definitions in case of alternative name
      if {[info exists modalt] && [info exists g_symbolHash($modalt)]} {
         set sym_list [lsort -dictionary -unique [list {*}$sym_list\
            {*}$g_symbolHash($modalt)]]
         reportDebug "set symbols '$sym_list' to $mod and $modalt"
         set g_symbolHash($mod) $sym_list
         set g_symbolHash($modalt) $sym_list
      }

      # dictionary-sort symbols and remove eventual duplicates
      set sym_list [lsort -dictionary -unique [list {*}$sym_list $symver]]

      # propagate symbols in g_symbolHash and g_moduleVersion toward the
      # resolution path, handle that locally if we still work on same
      # modulename, call for a proper resolution as soon as we change of
      # module to get this new resolution registered
      foreach modres $res_path {
         lassign [getModuleNameVersion $modres] modfull modresname
         if {$modname eq $modresname} {
            if {[info exists g_symbolHash($modres)]} {
               set modres_sym_list [lsort -dictionary -unique [list\
                  {*}$g_symbolHash($modres) {*}$sym_list]]
            } else {
               set modres_sym_list $sym_list
            }
            # sync symbols of alternative name if any
            if {[info exists ::g_moduleAltName($modres)]} {
               set altmodres $::g_moduleAltName($modres)
               reportDebug "set symbols '$modres_sym_list' to $modres and\
                  $altmodres"
               set g_symbolHash($altmodres) $modres_sym_list
            } else {
               reportDebug "set symbols '$modres_sym_list' to $modres"
            }
            set g_symbolHash($modres) $modres_sym_list

            # register symbolic version for querying in g_moduleVersion
            foreach symelt $sym_list {
               set modvers $modresname/$symelt
               reportDebug "module-version $modvers = $modres"
               set ::g_moduleVersion($modvers) $modres
               set ::g_sourceVersion($modvers) [currentState modulefile]
               # record eventual missing resolution
               if {![info exists g_moduleResolved($modvers)]} {
                  set g_moduleResolved($modvers) $modres
                  reportDebug "$modvers resolved to $modres"
                  lappend g_resolvedHash($modres) $modvers
               }
            }
         # as we change of module name a proper resolution call should be
         # made (see below) and will handle the rest of the resolution path
         } else {
            set need_set_res 1
            break
         }
      }
   # when registering an alias, existing symbols on alias source name should
   # be broadcast along the resolution path with a proper resolution call
   # (see below)
   } else {
      lassign [getModuleNameVersion $target] modres modresname
      set need_set_res 1
   }

   # resolution needed to broadcast symbols along resolution path without
   # altering initial path already set for these symbols
   if {[info exists need_set_res]} {
      foreach symelt $sym_list {
         set modvers $modresname/$symelt
         reportDebug "set resolution for $modvers"
         setModuleResolution $modvers $modres $symelt 0
      }
   }

   return 1
}

# retrieve all names that resolve to passed mod
proc getAllModuleResolvedName {mod {flag_type 0} {modspec {}}} {
   set namelist {}
   set resmodlist {}
   set icase [isIcase]
   defineModEqProc $icase [getConf extended_default]

   # get parent directories of mod
   foreach modelt [split $mod /] {
      if {[info exists modroot]} {
         append modroot /
      }
      append modroot $modelt
      lappend resmodlist $modroot
   }

   # add additionally all the altnames set on directories, parents of mod
   # or on distant directories whose default version resolves to mod
   for {set i 0} {$i < [llength $resmodlist]} {incr i 1} {
      set modelt [getArrayKey ::g_resolvedHash [lindex $resmodlist $i] $icase]
      if {[info exists ::g_resolvedHash($modelt)]} {
         foreach resmod $::g_resolvedHash($modelt) {
            # if alternative name corresponds to one root name test if default
            # symbol is hidden
            set resmoddfl [expr {[lsearch -exact $resmodlist $resmod] != -1 ?\
               "$resmod/default" : {}}]
            # if alternative name corresponds to default symbol and is hidden
            # test if query matches bare module name
            set resmodpar [expr {[file tail $resmod] eq {default} ? [file\
               dirname $resmod] : {}}]

            # if modelt is not a parent directory of mod, check its resolution
            # points to mod (directly for alias/sym or indirectly for dir
            # whose default version bridge resolution toward mod)
            # if modspec arg is set, exclude hidden entries not explicitly
            # matching modspec. auto symbols cannot be hidden
            if {($modspec eq {} || ([info exists ::g_autoSymbol($resmod)] &&\
               $::g_autoSymbol($resmod) eq $mod) || (![isModuleHidden $resmod\
               $modspec] && ($resmoddfl eq {} || ![isModuleHidden $resmoddfl\
               $modspec])) || [modEq $modspec $resmod eqspec] || ($resmodpar\
               ne {} && [modEq $modspec $resmodpar eqspec])) && ([modEq\
               $modelt $mod eqstart] || $::g_moduleResolved($resmod) eq $mod\
               || $mod eq [lindex [getPathToModule\
               $::g_moduleResolved($resmod) {} 0] 1])} {
               # prefix automatically generated syms with type flag if asked
               if {$flag_type && [info exists ::g_moduleAlias($resmod)]} {
                  appendNoDupToList namelist al|$resmod
               } elseif {$flag_type && [info exists\
                  ::g_autoSymbol($resmod)]} {
                  appendNoDupToList namelist as|$resmod
               } else {
                  appendNoDupToList namelist $resmod
               }

               unset modroot
               foreach reselt [split [file dirname $resmod] /] {
                  if {[info exists modroot]} {
                     append modroot /
                  }
                  append modroot $reselt
                  appendNoDupToList resmodlist $modroot
               }
            }
         }
      }
   }
   return $namelist
}

# build list for a given loaded module of its alternative names plus its
# simplified names (name minus version or any subdir if this version or subdir
# are the implicit default for module)
proc getLoadedAltAndSimplifiedName {mod} {
   if {[isModuleFullPath $mod]} {
      set namelist [list $mod]
   # use already computed name list if any
   } elseif {[info exists ::g_loadedAltAndSimplifiedName($mod)]} {
      set namelist $::g_loadedAltAndSimplifiedName($mod)
   } else {
      # get recorded alternative name
      set namelist [getLoadedAltname $mod]
      # also look through modpaths for simplified mod name
      set modpathlist [getModulePathList]
      if {[llength $modpathlist] > 0} {
         set modfile [getModulefileFromLoadedModule $mod]
         set parentmod [file dirname $mod]
         # simplify to parent name as long as it resolves to current mod
         while {$parentmod ne {.}} {
            lassign [getPathToModule $parentmod $modpathlist 0] parentfile
            if {$parentfile eq $modfile} {
               lappend namelist $parentmod
               set parentmod [file dirname $parentmod]
            } else {
               set parentmod .
            }
         }
      }
      # recorded computed name list for later use
      set ::g_loadedAltAndSimplifiedName($mod) $namelist
   }
   return $namelist
}

# return list of loaded modules by parsing LOADEDMODULES env variable
proc getLoadedModuleList {{filter_empty 1}} {
   set modlist [list]
   foreach mod [split [get-env LOADEDMODULES] [getState path_separator]] {
      # ignore empty element
      if {$mod ne {} || !$filter_empty} {
         lappend modlist $mod
      }
   }
   return $modlist
}

# return list of loaded module files by parsing _LMFILES_ env variable
proc getLoadedModuleFileList {} {
   set modfilelist [list]
   foreach modfile [split [get-env _LMFILES_] [getState path_separator]] {
      # ignore empty element
      if {$modfile ne {}} {
         lappend modfilelist $modfile
      }
   }
   return $modfilelist
}

# return list of declared source-sh by loaded module by parsing
# __MODULES_LMSOURCESH
proc getLoadedModuleSourceShList {} {
   set modsrcshlist [list]
   set pathsep [getState path_separator]
   set sub1sep [getState sub1_separator]
   set sub2sep [getState sub2_separator]
   set unsermap [list <EnvModEscPS> $pathsep <EnvModEscS1> $sub1sep\
      <EnvModEscS2> $sub2sep]

   foreach modsrcshser [split [get-env __MODULES_LMSOURCESH] $pathsep] {
      set srcshlist [split $modsrcshser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $srcshlist] > 1} {
         set modsrcsh {}
         # keep first arg as string and other args as lists
         foreach srcsh $srcshlist {
            # unescape delimiter chars used in content
            if {[llength $modsrcsh] == 0} {
               lappend modsrcsh [string map $unsermap $srcsh]
            } else {
               lappend modsrcsh [string map $unsermap [split $srcsh $sub2sep]]
            }
         }
         lappend modsrcshlist $modsrcsh
      }
   }
   return $modsrcshlist
}

# return list of loaded module declared conflict by parsing
# __MODULES_LMCONFLICT
proc getLoadedModuleConflictList {} {
   set modconlist [list]
   # get sub level separator that serialize second level of info in env var
   set sub1sep [getState sub1_separator]
   foreach modconser [split [get-env __MODULES_LMCONFLICT] [getState\
      path_separator]] {
      # recover range specifier ':' from its serialized form '<'
      set conlist [split [string map {< :} $modconser] $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $conlist] > 1} {
         lappend modconlist $conlist
      }
   }
   return $modconlist
}

# return list of loaded module declared prereq by parsing __MODULES_LMPREREQ
proc getLoadedModulePrereqList {} {
   set modprelist [list]
   set sub1sep [getState sub1_separator]
   # get sub sub level separator that serialize third level of info in env var
   set sub2sep [getState sub2_separator]
   foreach modpreser [split [get-env __MODULES_LMPREREQ] [getState\
      path_separator]] {
      # recover range specifier ':' from its serialized form '<'
      set prelist [split [string map {< :} $modpreser] $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $prelist] > 1} {
         set modpre {}
         # keep first arg as string and other args as lists
         foreach pre $prelist {
            if {[llength $modpre] == 0} {
               lappend modpre $pre
            } else {
               lappend modpre [split $pre $sub2sep]
            }
         }

         lappend modprelist $modpre
      }
   }
   return $modprelist
}

# return list of loaded module declared altnames by parsing
# __MODULES_LMALTNAME
proc getLoadedModuleAltnameList {} {
   set modaltlist [list]
   set sub1sep [getState sub1_separator]
   foreach modaltser [split [get-env __MODULES_LMALTNAME] [getState\
      path_separator]] {
      set altlist [split $modaltser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $altlist] > 1} {
         lappend modaltlist $altlist
      }
   }
   return $modaltlist
}

# return list of loaded module declared variant by parsing __MODULES_LMVARIANT
proc getLoadedModuleVariantList {} {
   set modvrspeclist [list]
   set sub1sep [getState sub1_separator]
   # get sub sub level separator that serialize third level of info in env var
   set sub2sep [getState sub2_separator]
   foreach modvrspecser [split [get-env __MODULES_LMVARIANT] [getState\
      path_separator]] {
      set vrspeclist [split $modvrspecser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $vrspeclist] > 1} {
         set modvrspec {}
         # keep first arg as string and other args as lists
         foreach vrspec $vrspeclist {
            if {[llength $modvrspec] == 0} {
               lappend modvrspec $vrspec
            # ignore empty variant spec
            } elseif {[llength [set vrspecli [split $vrspec $sub2sep]]] > 0} {
               lappend modvrspec $vrspecli
            }
         }

         lappend modvrspeclist $modvrspec
      }
   }
   return $modvrspeclist
}

# return list of loaded module declared tags by parsing __MODULES_LMTAG
proc getLoadedModuleTagList {} {
   set modtaglist [list]
   set sub1sep [getState sub1_separator]
   foreach modtagser [split [get-env __MODULES_LMTAG] [getState\
      path_separator]] {
      set taglist [split $modtagser $sub1sep]
      # ignore empty element (1 is meaningless as first elt is loaded mod)
      if {[llength $taglist] > 1} {
         lappend modtaglist $taglist
      }
   }
   return $modtaglist
}

# return list of module paths by parsing MODULEPATH env variable
# behavior param enables to exit in error when no MODULEPATH env variable
# is set. by default an empty list is returned if no MODULEPATH set
# resolv_var param tells if environment variable references in path elements
# should be resolved or passed as-is in result list
# set_abs param applies an absolute path name conversion to path elements
# if enabled
proc getModulePathList {{behavior returnempty} {resolv_var 1} {set_abs 1}} {
   if {[info exists ::env(MODULEPATH)]} {
      set modpathlist [list]
      foreach modpath [split $::env(MODULEPATH) [getState path_separator]] {
         # ignore empty element
         if {$modpath ne {}} {
            if {$resolv_var} {
               set modpath [resolvStringWithEnv $modpath]
            }
            if {$set_abs} {
               set modpath [getAbsolutePath $modpath]
            }
            appendNoDupToList modpathlist $modpath
         }
      }
      return $modpathlist
   } elseif {$behavior eq {exiterronundef}} {
      reportErrorAndExit {No module path defined}
   } else {
      return {}
   }
}

proc getModuleTag {mod tag {cmp eqstart} {return_rule 0}} {
   set modroot [getModuleRootFromVersSpec $mod]
   # look if mod matches one of the module specs applying to mod root
   if {[info exists ::g_moduleTagRoot($modroot)]} {
      set idx [lsearch -exact $::g_moduleTagRoot($modroot) $tag]
      if {$idx == -1} {
         return {}
      } else {
         foreach {tmodspec tprops} [lindex $::g_moduleTagRootSpec($modroot)\
            $idx] {
            # first matching module spec wins with its properties
            if {[modEq $tmodspec $mod $cmp 1 0 1]} {
               if {$return_rule} {
                  return $tmodspec
               } else {
                  return [list $tag $tprops]
               }
            }
         }
      }
   }
   return {}
}

proc isModuleTagged {mod tag {collected 0}} {
   # retrieve tag information from collected tags or raw info
   if {$collected} {
      return [expr {$tag in [getTagList $mod]}]
   } else {
      return [expr {[llength [getModuleTag $mod $tag]] != 0}]
   }
}

proc getTaggedLoadedModuleList {tag} {
   set modlist [list]
   foreach mod [getLoadedModuleList] {
      if {[isModuleTagged $mod $tag 1]} {
         lappend modlist $mod
      }
   }
   return $modlist
}

proc getModuleTagProp {mod tag prop} {
   set ret {}
   array set tags [getModuleTag $mod $tag]

   if {[info exists tags($tag)]} {
      array set props $tags($tag)
      if {[info exists props($prop)]} {
         set ret $props($prop)
      }
   }

   return $ret
}

# Gather all tags applying to a given module
proc collectModuleTags {mod} {
   set modroot [getModuleRootFromVersSpec $mod]
   # look if mod matches one of the module specs applying to mod root
   if {[info exists ::g_moduleTagRoot($modroot)]} {
      for {set i 0} {$i < [llength $::g_moduleTagRoot($modroot)]} {incr i} {
         set tag [lindex $::g_moduleTagRoot($modroot) $i]
         # skip already known tag
         if {![info exists ::g_tagHash($mod)] || $tag ni $::g_tagHash($mod)} {
            foreach {tmodspec tprops} [lindex\
               $::g_moduleTagRootSpec($modroot) $i] {
               # add tag if one modspec matches mod
               if {[modEq $tmodspec $mod eqstart 1 0 1]} {
                  setModuleTag $mod $tag
                  break
               }
            }
         }
      }
   }
}

proc setModuleTag {mod args} {
   lappend ::g_tagHash($mod) {*}$args
}

proc getTagList {mod {serialized 0}} {
   if {[info exists ::g_tagHash($mod)]} {
      if {$serialized} {
         # get tag info as a string that can be registered in an env var
         # remove loaded/hidden tags from the list to register
         lassign [getDiffBetweenList $::g_tagHash($mod) [list loaded hidden]]\
            tag_list
         if {[llength $tag_list] > 0} {
            # extract mod name and version from full spec with variants
            set ret [join [list [getModuleNameAndVersFromVersSpec $mod]\
               {*}$tag_list] [getState sub1_separator]]
         } else {
            set ret {}
         }
      } else {
         set ret [lsort -dictionary $::g_tagHash($mod)]
         reportDebug "'$mod' has tag list '$ret'"
      }
   } else {
      set ret {}
   }
   return $ret
}

proc abbrevTagList {taglist} {
   set ret [list]
   foreach tag $taglist {
      if {[info exists ::g_tagAbbrev($tag)]} {
         # empty string abbrev means no tag report
         if {$::g_tagAbbrev($tag) ne {}} {
            lappend ret $::g_tagAbbrev($tag)
         }
      } else {
         lappend ret $tag
      }
   }
   return [lsort -dictionary $ret]
}

proc getVariantList {mod {report 0} {excl_dflval 0} {from_spec 0}} {
   # fetch variants from module specification
   if {$from_spec} {
      set vrspeclist [getVariantListFromVersSpec $mod]
   # or from variant set after module being loaded
   } elseif {[info exists ::g_loadedModuleVariant($mod)]} {
      set vrspeclist $::g_loadedModuleVariant($mod)
   } else {
      set vrspeclist {}
   }

   # use array to make variant unique
   foreach vrspec $vrspeclist {
      lassign $vrspec vrname vrvalue vrvalisbool vrvalisdfl
      # correct is-default value if invalid
      if {![string is integer -strict $vrvalisdfl]} {
         set vrvalisdfl 0
      }
      # correct is-boolean value if invalid
      if {![string is boolean -strict $vrvalisbool]} {
         set vrvalisbool 0
      }
      set vrisbool($vrname) $vrvalisbool
      # correct boolean value if invalid
      if {$vrvalisbool && ![string is boolean -strict $vrvalue]} {
         set vrvalue 0
      }
      # do not return variant if value is default, unless if variant was
      # specifically set to default value (isdfl=1) and we only want to
      # exclude the automatically set default value (excl_dflval=2)
      if {$excl_dflval > 0} {
         if {$vrvalisdfl == 0 || ($excl_dflval == 2 && $vrvalisdfl == 1)} {
            set vrarr($vrname) $vrvalue
         # nullify previous duplicate definition if this one is default
         } elseif {[info exists vrarr($vrname)]} {
            unset vrarr($vrname)
         }
      } else {
         # is-default hint has to be transmitted on report mode '3'
         if {$report == 3} {
            set vrarr($vrname) [list $vrvalue [expr {$vrvalisdfl > 0 &&\
               $vrvalisdfl < 3}]]
         } else {
            set vrarr($vrname) $vrvalue
         }
      }
   }
   # sort variant to report
   set ret {}
   foreach vrname [lsort -dictionary [array names vrarr]] {
      switch -- $report {
         4 {lappend ret $vrname $vrarr($vrname) $vrisbool($vrname)}
         3 {lappend ret $vrname [lindex $vrarr($vrname) 0] [lindex\
            $vrarr($vrname) 1]}
         2 {lappend ret [list $vrname $vrarr($vrname) $vrisbool($vrname)]}
         1 - 5 - 6 {
            if {$vrisbool($vrname)} {
               if {$vrarr($vrname)} {
                  lappend ret +$vrname
                  # track if +var variant has been reported to build key
                  if {![info exists ::g_used_va(on)]} {
                     set ::g_used_va(on) 1
                  }
               } else {
                  lappend ret -$vrname
                  # track if -var variant has been reported to build key
                  if {![info exists ::g_used_va(off)]} {
                     set ::g_used_va(off) 1
                  }
               }
            # use defined shortcut to report if any set for this variant
            # ignore shortcut if report=5 (when saving collection)
            } elseif {$report != 5 && [info exists\
               ::g_variantShortcut($vrname)]} {
               set sc $::g_variantShortcut($vrname)
               lappend ret $sc$vrarr($vrname)
               # track if variant shortcut has been reported to build key
               # unless if key should not be updated (report mode '6')
               if {$report != 6 && ![info exists ::g_used_va($sc)]} {
                  set ::g_used_va($sc) $vrname
               }
            } else {
               lappend ret $vrname=$vrarr($vrname)
               # track if var=val variant has been reported to build key
               # unless if key should not be updated (report mode '6')
               if {$report != 6 && ![info exists ::g_used_va(val)]} {
                  set ::g_used_va(val) 1
               }
            }
         }
         0 {lappend ret $vrname $vrarr($vrname)}
      }
   }

   return $ret
}

proc isModuleDotHidden {mod} {
   foreach elt [split $mod /] {
      if {[string index $elt 0] eq {.}} {
         return 1
      }
   }
   return 0
}

proc getModuleHidingLevel {mod} {
   set modroot [lindex [file split $mod] 0]
   # look if mod matches one of the hidden module specs applying to mod root
   if {[info exists ::g_moduleHideRoot($modroot)]} {
      for {set lvl 2} {$lvl > -1} {incr lvl -1} {
         foreach hmodspec [lindex $::g_moduleHideRoot($modroot) $lvl] {
            if {[modEq $hmodspec $mod eqstart 1 0 1 {}]} {
               return $lvl
            }
         }
      }
   }
   return -1
}

proc setModspecHidingLevel {modspec lvl} {
   # skip record if an higher hiding level is already set
   if {![info exists ::g_moduleHide($modspec)] || $lvl >\
      $::g_moduleHide($modspec)} {
      reportDebug "Record hidden module specification '$modspec' (lvl=$lvl)"
      set ::g_moduleHide($modspec) $lvl

      # record hidden mod spec for mod root to optimize search
      set modroot [getModuleRootFromVersSpec $modspec]
      if {![info exists ::g_moduleHideRoot($modroot)]} {
         set ::g_moduleHideRoot($modroot) [list [list] [list] [list]]
      }
      # record in list corresponding to hidden level to search from strongest
      # to weakest hidden level
      set hiderootlvllist [lindex $::g_moduleHideRoot($modroot) $lvl]
      lappend hiderootlvllist $modspec
      lset ::g_moduleHideRoot($modroot) $lvl $hiderootlvllist
   }
}

# test if mod is declared hidden or has one element in its name starting with
# dot character. mod is considered hidden depending on their hiding level,
# current search query and hiding threshold. when retdetails option is
# enabled, mod hiding level and query match hind are also returned
proc isModuleHidden {mod {modspec {}} {retdetails 0}} {
   set defhidlvl [set hidlvl [getModuleHidingLevel $mod]]
   if {$hidlvl >= [getState hiding_threshold]} {
      # soft hidden mods are considered matched if their root name matches
      # search query, other kind of hidden mods must fully matches query
      set hidmatch [expr {$hidlvl == 0 ? [expr {[modStartNb $mod $modspec]\
         > 0}] : [modEq $modspec $mod eqspec]}]
   } else {
      set hidlvl -1
      set hidmatch 0
   }
   if {$hidlvl < 1 && [set isdot [isModuleDotHidden $mod]] && [getState\
      hiding_threshold] < 1} {
      set hidlvl 1
      # dot hidden are considered matched if remaining string part after
      # search query is not dot hidden
      set hidmatch [expr {[set i [modStartNb $mod $modspec]] > 0 &&\
         ![isModuleDotHidden [join [lrange [file split $mod] $i end] /]]}]
   }
   # hidden if hiding level greater or equal hiding threshold and not
   # matched or if matched hard hiding level are kept hidden
   set ishid [expr {$hidlvl != -1 && (!$hidmatch || $hidlvl > 1)}]

   # is module hidden by definition (whatever hiding threshold or match)
   set hidbydef [expr {$defhidlvl > 0 || ([info exists isdot] && $isdot)}]

   set ret [expr {$retdetails ? [list $hidlvl $hidmatch $hidbydef $ishid]\
      : $ishid}]

   return $ret
}

# check if module name is specified as a full pathname (not a name relative
# to a modulepath)
proc isModuleFullPath {mod} {
   return [regexp {^(|\.|\.\.)/} $mod]
}

# check if a module corresponds to a virtual module (module name
# does not corresponds to end of the modulefile name)
proc isModuleVirtual {mod modfile} {
   return [expr {[string first $mod $modfile end-[string length $mod]] == -1}]
}

# Return the full pathname and modulename to the module.
# Resolve aliases and default versions if the module name is something like
# "name/version" or just "name" (find default version).
proc getPathToModule {mod {indir {}} {report_issue 1} {look_loaded no}\
   {excdir {}}} {
   reportDebug "finding '$mod' in '$indir' (report_issue=$report_issue,\
      look_loaded=$look_loaded, excdir='$excdir')"

   set vrlist [getVariantList $mod 1 0 1]
   if {$mod eq {}} {
      set retlist [list {} {} {} none [getEmptyNameMsg module]]
   # try first to look at loaded modules if enabled to find maching module
   # or to find a closest match (used when switching with single name arg)
   } elseif {$look_loaded ne {no}} {
      switch -- $look_loaded {
         match {set getLoadedNameProc getLoadedMatchingName}
         close {set getLoadedNameProc getLoadedWithClosestName}
      }
      if {[set lm [$getLoadedNameProc $mod]] ne {}} {
         set vrlist [getVariantList $lm 1]
         set retlist [list [getModulefileFromLoadedModule $lm] $lm]
      } else {
         set retlist [list {} [getModuleNameAndVersFromVersSpec $mod]\
            $mod notloaded]
      }
   # Check for $mod specified as a full pathname
   } elseif {[isModuleFullPath $mod]} {
      set mod [getAbsolutePath $mod]
      # note that a raw filename as an argument returns the full
      # path as the module name
      lassign [checkValidModule $mod] check_valid check_msg
      switch -- $check_valid {
         true {
            set retlist [list $mod $mod]
         }
         invalid - accesserr {
            set retlist [list {} $mod $mod $check_valid $check_msg $mod]
         }
      }
   } else {
      set dir_list [expr {$indir ne {} ? $indir : [getModulePathList\
         exiterronundef]}]
      # remove excluded directories (already searched)
      foreach dir $excdir {
         set dir_list [replaceFromList $dir_list $dir]
      }

      set icase [isIcase]
      defineGetEqArrayKeyProc $icase [getConf extended_default] [getConf\
         implicit_default]

      # Now search for $mod in module paths
      set modspec $mod
      foreach dir $dir_list {
         # get list of modules corresponding to searched query
         array unset mod_list
         array set mod_list [getModules $dir $mod 0 [list rc_defs_included\
            resolve]]

         set prevmod {}
         set mod_res {}
         # loop to resolve correct modulefile in case specified mod is a
         # directory that should be analyzed to get default mod in it
         while {$prevmod ne $mod} {
            set mod [getEqArrayKey mod_list $mod]
            set prevmod $mod
            if {[info exists mod_list($mod)]} {
               switch -- [lindex $mod_list($mod) 0] {
                  alias - version {
                     set newmod [resolveModuleVersionOrAlias $mod $icase]
                     if {[info exists mod_list($newmod)]} {
                        set mod $newmod
                     } else {
                        # add specified variants to current resolution
                        if {[llength $vrlist] > 0} {
                           lassign [parseModuleSpecification 0 $newmod\
                              {*}$vrlist] newmod
                        }
                        # restart search on new modulename, constrained to
                        # specified dir if set, if not found in current res
                        return [getPathToModule $newmod $indir $report_issue]
                     }
                  }
                  directory {
                     # is implicit default disabled and none explicitly set?
                     if {[lindex $mod_list($mod) 1] eq {}} {
                        set retlist [list {} $mod $mod none "No default\
                           version defined for '$mod'"]
                     } else {
                        # Move to default element in directory
                        set mod $mod/[lindex $mod_list($mod) 1]
                        # restart search if default element is an hidden dir
                        if {![info exists mod_list($mod)] && [isModuleHidden\
                           $mod $modspec]} {
                           # add specified variants to current resolution
                           if {[llength $vrlist] > 0} {
                              lassign [parseModuleSpecification 0 $mod\
                                 {*}$vrlist] mod
                           }
                           return [getPathToModule $mod $indir $report_issue]
                        }
                     }
                  }
                  modulefile {
                     # If mod was a file in this path, return that file
                     set retlist [list $dir/$mod $mod]
                  }
                  virtual {
                     # return virtual name with file it targets
                     set retlist [list [lindex $mod_list($mod) 2] $mod]
                  }
                  invalid - accesserr {
                     # may found mod but issue, so end search with error
                     set retlist [list {} $mod $mod {*}$mod_list($mod)]
                  }
               }
            }
         }
         # break loop if found something (valid or invalid module)
         # elsewhere go to next path
         if {[info exists retlist]} {
            break
         }
      }
   }

   # set result if nothing found
   if {![info exists retlist]} {
      set retlist [list {} $mod $mod none "Unable to locate a modulefile for\
         '$mod'"]
   } else {
      # build module name and variant with variant specified
      if {[llength $vrlist] > 0} {
         lassign [parseModuleSpecification 0 [lindex $retlist 1] {*}$vrlist]\
            modnamevr
      } else {
         set modnamevr [lindex $retlist 1]
      }
      # update result if forbidden
      if {[isModuleTagged $modnamevr forbidden]} {
         set retlist [list {} [lindex $retlist 1] [lindex $retlist 2]\
            accesserr [getForbiddenMsg $modnamevr]]
      }
   }
   if {[lindex $retlist 0] ne {}} {
      lappend retlist $modnamevr
      reportTrace "'[lindex $retlist 1]' ([lindex $retlist 0]) matching\
         '$mod'" {Select module}
   # no error if we look at loaded modules and passed mod not found loaded
   } elseif {[lindex $retlist 3] ne {notloaded} && $report_issue} {
      reportIssue {*}[lrange $retlist 3 5]
   }
   return $retlist
}

proc isModuleLoaded {mod} {
   cacheCurrentModules

   return [info exists ::g_loadedModules($mod)]
}

proc getModulefileFromLoadedModule {mod} {
   if {[isModuleLoaded $mod]} {
      return $::g_loadedModules($mod)
   } else {
      return {}
   }
}

proc isModulefileLoaded {modfile} {
   cacheCurrentModules

   return [info exists ::g_loadedModuleFiles($modfile)]
}

proc getModuleFromLoadedModulefile {modfile {idx all}} {
   if {[isModulefileLoaded $modfile]} {
      if {$idx eq {all}} {
         return $::g_loadedModuleFiles($modfile)
      } else {
         return [lindex $::g_loadedModuleFiles($modfile) $idx]
      }
   } else {
      return {}
   }
}

proc isModuleLoading {mod} {
   return [expr {$mod in [getLoadingModuleList]}]
}

proc isModulefileLoading {modfile} {
   return [expr {$modfile in [getLoadingModuleFileList]}]
}

proc getModuleFromLoadingModulefile {modfile {idx all}} {
   if {[isModulefileLoading $modfile]} {
      set loadingmodlist [getLoadingModuleList]
      foreach i [lsearch -all -exact [getLoadingModuleFileList] $modfile] {
         lappend modlist [lindex $loadingmodlist $i]
      }

      if {$idx eq {all}} {
         return $modlist
      } else {
         return [lindex $modlist $idx]
      }
   } else {
      return {}
   }
}

proc setLoadedModule {mod modfile uasked {modvr {}}} {
   set ::g_loadedModules($mod) $modfile
   # a loaded modfile may correspond to multiple loaded virtual modules
   lappend ::g_loadedModuleFiles($modfile) $mod
   # record if mod has been asked by user and relative loaded/auto-loaded tag
   if {$uasked} {
      setModuleTag $mod loaded
      # also record tag for module designation including variants if specified
      if {$modvr ne {} && $mod ne $modvr} {
         setModuleTag $modvr loaded
      }
   # do not register auto-loaded tag if already done (when called from
   # cacheCurrentModules proc)
   } else {
      if {![isModuleTagged $mod auto-loaded 1]} {
         setModuleTag $mod auto-loaded
      }
      if {$modvr ne {} && $mod ne $modvr && ![isModuleTagged $modvr\
         auto-loaded 1]} {
         setModuleTag $modvr auto-loaded
      }
   }
   # build dependency chain
   setModuleDependency $mod
}

proc unsetLoadedModule {mod modfile} {
   unset ::g_loadedModules($mod)
   # a loaded modfile may correspond to multiple loaded virtual modules
   if {[llength $::g_loadedModuleFiles($modfile)] == 1} {
      unset ::g_loadedModuleFiles($modfile)
   } else {
      set ::g_loadedModuleFiles($modfile) [replaceFromList\
         $::g_loadedModuleFiles($modfile) $mod]
   }
   # update dependencies
   unsetModuleDependency $mod
}

# return the currently loaded module whose name is the closest to the
# name passed as argument. if no loaded module match at least one part
# of the passed name, an empty string is returned.
proc getLoadedWithClosestName {name} {
   set ret {}
   set retmax 1

   if {[isModuleFullPath $name]} {
      set fullname [getAbsolutePath $name]
      # if module is passed as full modulefile path name, get corresponding
      # short name from used modulepaths
      if {[set shortname [findModuleNameFromModulefile $fullname]] ne {}} {
         set nametosplit $shortname
      # or look at lmfile names to return the eventual exact match
      } else {
         # module may be loaded with its full path name
         if {[isModuleLoaded $fullname]} {
            set ret $fullname
         # or name corresponds to the _lmfiles_ entry of a virtual modules in
         # which case lastly loaded virtual module is returned
         } elseif {[isModulefileLoaded $fullname]} {
            set ret [getModuleFromLoadedModulefile $fullname end]
         }
      }
   } else {
      set nametosplit $name
   }

   if {[info exists nametosplit]} {
      cacheCurrentModules
      set icase [isIcase]
      defineModStartNbProc $icase
      defineModEqProc $icase [getConf extended_default]
      # compare name to each currently loaded module name
      foreach mod [getLoadedModuleList] {
         # if module loaded as fullpath but test name not, try to get loaded
         # mod short name (with currently used modulepaths) to compare it
         if {[isModuleFullPath $mod] && [set modname\
            [findModuleNameFromModulefile $mod]] ne {}} {
            # no alt name to retrieve if module has been loaded full path
            set matchmodlist [list $modname]
         } else {
            # add alternative names of mod to the matching list
            set matchmodlist [list $mod {*}[getLoadedAltname $mod]]
         }

         # compare each element of the name to find closest answer. in case of
         # equality, last loaded module will be returned as it overwrites
         # previously found value
         foreach matchmod $matchmodlist {
            if {[set i [modStartNb $matchmod $nametosplit]] >= $retmax} {
               set retmax $i
               set ret $mod
               break
            }
         }
      }
   }

   reportDebug "'$ret' closest to '$name'"
   return $ret
}

# return the currently loaded module whose name is equal or include the name
# passed as argument. if no loaded module match, an empty string is returned.
# loading: look at currently loading modules instead of loaded if loading == 1
# lmlist: only take into account passed loaded module list not all loaded mods
proc getLoadedMatchingName {name {behavior {}} {loading 0} {lmlist {}}} {
   set ret {}
   set retmax 0
   # get default behavior from unload_match_order config
   if {$behavior eq {}} {
      set behavior [getConf unload_match_order]
   }

   # use loading-specific procedures instead of loaded-specific ones
   if {$loading} {
      set isModulefileLoaded isModulefileLoading
      set getModuleFromLoadedModulefile getModuleFromLoadingModulefile
      set getLoadedModuleList getLoadingModuleList
   } else {
      set isModulefileLoaded isModulefileLoaded
      set getModuleFromLoadedModulefile getModuleFromLoadedModulefile
      set getLoadedModuleList getLoadedModuleList
   }

   # fetch currently loaded/loading module name is no list provided
   if {[llength $lmlist] == 0} {
      set lmlist [$getLoadedModuleList]
   }

   # if module is passed as full modulefile path name, look at lmfile names
   # to return the eventual exact match
   if {[isModuleFullPath $name]} {
      set mod [getAbsolutePath $name]
      # if module is loaded with its full path name loadedmodules entry is
      # equivalent to _lmfiles_ corresponding entry so only check _lmfiles_
      if {[$isModulefileLoaded $mod]} {
         # a loaded modfile may correspond to multiple loaded virtual modules
         switch -- $behavior {
            returnlast {
               # the last loaded/loading module will be returned
               set ret [$getModuleFromLoadedModulefile $mod end]
            }
            returnfirst {
               # the first loaded/loading module will be returned
               set ret [$getModuleFromLoadedModulefile $mod 0]
            }
            returnall {
               # all loaded/loading modules will be returned
               set ret [$getModuleFromLoadedModulefile $mod]
            }
         }
      }
   } elseif {$name ne {}} {
      defineModEqProc [isIcase] [getConf extended_default] [expr {!$loading}]
      # compare name to each currently loaded/loading module name, if multiple
      # mod match name:
      foreach mod $lmlist {
         # if module loaded as fullpath but test name not, try to get loaded
         # mod short name (with currently used modulepaths) to compare it
         if {[isModuleFullPath $mod] && [set modname\
            [findModuleNameFromModulefile $mod]] ne {}} {
            set matchmod $modname
         } else {
            set matchmod $mod
         }
         if {[modEq $name $matchmod eqstart 1 [expr {$loading ? 1 : 2}] 1]} {
            switch -- $behavior {
               returnlast {
                  # the last loaded module will be returned
                  set ret $mod
               }
               returnfirst {
                  # the first loaded module will be returned
                  set ret $mod
                  break
               }
               returnall {
                  # all loaded modules will be returned
                  lappend ret $mod
               }
            }
         }
      }
   }

   reportDebug "'$ret' matches '$name'"
   return $ret
}

proc setLoadedSourceSh {mod args} {
   foreach arg $args {
      # each arg is a list with source-sh call string at index 0 and resulting
      # modulefile commands at all later index positions
      set shtomodargs [lindex $arg 0]
      set modcontent [lrange $arg 1 end]
      if {![info exists ::g_loadedModuleSourceSh($mod)] || $shtomodargs ni\
         $::g_loadedModuleSourceSh($mod)} {
         # filter alias or function definition not to record them
         set filtmodcont [list]
         foreach modcmdlist $modcontent {
            set modcmd [lindex $modcmdlist 0]
            if {$modcmd eq {set-alias} || $modcmd eq {set-function}} {
               # set an empty body to make valid unset-* call
               lappend filtmodcont [list {*}[lrange $modcmdlist 0 1] {}]
            } else {
               lappend filtmodcont $modcmdlist
            }
         }
         lappend ::g_loadedModuleSourceSh($mod) $shtomodargs $filtmodcont
      }
   }
}

proc unsetLoadedSourceSh {mod} {
   if {[info exists ::g_loadedModuleSourceSh($mod)]} {
      unset ::g_loadedModuleSourceSh($mod)
   }
}

proc getLoadedSourceSh {mod {serialized 0}} {
   set ret {}

   set pathsep [getState path_separator]
   set sub1sep [getState sub1_separator]
   set sub2sep [getState sub2_separator]
   set sermap [list $pathsep <EnvModEscPS> $sub1sep <EnvModEscS1>\
      $sub2sep <EnvModEscS2>]

   if {[info exists ::g_loadedModuleSourceSh($mod)]} {
      if {$serialized} {
         foreach {shtomodargs modcontent} $::g_loadedModuleSourceSh($mod) {
            # escape delimiter chars if used in content
            set shtomodargsser [string map $sermap $shtomodargs]
            set modcontentser [string map $sermap $modcontent]
            lappend modsrcsh $shtomodargsser$sub2sep[join $modcontentser\
               $sub2sep]
         }
         set ret [join [list $mod {*}$modsrcsh] $sub1sep]
      } else {
         set ret $::g_loadedModuleSourceSh($mod)
      }
   }

   return $ret
}

proc setLoadedConflict {mod args} {
   appendNoDupToList ::g_loadedModuleConflict($mod) {*}$args
}

proc unsetLoadedConflict {mod} {
   if {[info exists ::g_loadedModuleConflict($mod)]} {
      unset ::g_loadedModuleConflict($mod)
   }
}

proc getLoadedConflict {mod {serialized 0}} {
   set ret {}

   if {[info exists ::g_loadedModuleConflict($mod)]} {
      if {$serialized} {
         # get conflict info as a string that can be registered in an env var
         # translate range specifier ':' into '<' to distinguish from path sep
         set ret [string map {: <} [join [list $mod\
            {*}$::g_loadedModuleConflict($mod)] [getState sub1_separator]]]
      } else {
         set ret $::g_loadedModuleConflict($mod)
      }
   }

   return $ret
}

proc doesModuleConflict {mod} {
   set does 0
   set modconlist {}
   set moddecconlist {}
   defineModEqProc [isIcase] [getConf extended_default] 1
   # get module short name if loaded by its full pathname
   if {[set isfullpath [isModuleFullPath $mod]]} {
      set smod [findModuleNameFromModulefile $mod]
   }

   # check if any loaded module has declared a conflict
   foreach modcon [array names ::g_loadedModuleConflict] {
      # look if some loaded or loading modules correspond to conflict defined
      # by mod
      if {$modcon eq $mod || ($isfullpath && $modcon eq $smod)} {
         foreach withmod $::g_loadedModuleConflict($modcon) {
            # skip own reflexive conflict (look at mod main and alternative
            # names) and those already known
            if {![modEq $withmod $mod eqstart 1 2 1] && (!$isfullpath ||\
               ![modEq $withmod $smod eqstart 1 2 1]) && ([set lmmodlist\
               [getLoadedMatchingName $withmod returnall]] ne {} || [set\
               lmmodlist [getLoadedMatchingName $withmod returnall 1]] ne {})} {
               # multiple loaded module may match conflict declared name
               foreach lmmod $lmmodlist {
                  appendNoDupToList modconlist $lmmod
               }
               appendNoDupToList moddecconlist $withmod
               set does 1
            }
         }
      # other loaded module declared conflicts (skipping those already known)
      } elseif {$modcon ni $modconlist} {
         foreach withmod $::g_loadedModuleConflict($modcon) {
            # check if mod or one of its alt name match conflict
            if {[modEq $withmod $mod eqstart 1 2 1] || ($isfullpath &&\
               [modEq $withmod $smod eqstart 1 2 1])} {
               lappend modconlist $modcon
               lappend moddecconlist $modcon
               set does 1
               break
            }
         }
      }
   }

   reportDebug "'$mod' conflicts with '$modconlist' (declared as '$moddecconlist')"
   return [list $does $modconlist $moddecconlist]
}

proc setLoadedPrereq {mod args} {
   appendNoDupToList ::g_loadedModulePrereq($mod) {*}$args
}

proc unsetLoadedPrereq {mod} {
   if {[info exists ::g_loadedModulePrereq($mod)]} {
      unset ::g_loadedModulePrereq($mod)
   }
}

proc getLoadedPrereq {mod {serialized 0}} {
   set ret {}

   set sub2sep [getState sub2_separator]
   if {[info exists ::g_loadedModulePrereq($mod)]} {
      if {$serialized} {
         # get prereq info as a string that can be registered in an env var
         foreach pre $::g_loadedModulePrereq($mod) {
            lappend modpre [join $pre $sub2sep]
         }
         # translate range specifier ':' into '<' to distinguish from path sep
         set ret [string map {: <} [join [list $mod {*}$modpre]\
            [getState sub1_separator]]]
      } else {
         set ret $::g_loadedModulePrereq($mod)
      }
   }

   return $ret
}

proc setLoadedAltname {mod args} {
   foreach arg $args {
      switch -- [string range $arg 0 2] {
         al| {
            appendNoDupToList ::g_loadedModuleAliasAltname($mod) [string\
               range $arg 3 end]
         }
         as| {
            appendNoDupToList ::g_loadedModuleAutoAltname($mod) [string range\
               $arg 3 end]
         }
         default {
            appendNoDupToList ::g_loadedModuleAltname($mod) $arg
         }
      }
      if {[string range $arg 0 2] eq {as|}} {
      } else {
      }
   }
}

proc unsetLoadedAltname {mod} {
   if {[info exists ::g_loadedModuleAltname($mod)]} {
      unset ::g_loadedModuleAltname($mod)
   }
   if {[info exists ::g_loadedModuleAliasAltname($mod)]} {
      unset ::g_loadedModuleAliasAltname($mod)
   }
   if {[info exists ::g_loadedModuleAutoAltname($mod)]} {
      unset ::g_loadedModuleAutoAltname($mod)
   }
}

proc getLoadedAltname {mod {serialized 0} {typelist {sym alias autosym}}} {
   set ret {}

   if {[info exists ::g_loadedModuleAltname($mod)] && {sym} in $typelist} {
      set ret $::g_loadedModuleAltname($mod)
   }
   if {[info exists ::g_loadedModuleAliasAltname($mod)] && {alias} in\
      $typelist} {
      if {$serialized} {
         # add a 'al|' prefix to each alias to distinguish them when
         # serialized
         foreach altname $::g_loadedModuleAliasAltname($mod) {
            lappend ret al|$altname
         }
      } else {
         lappend ret {*}$::g_loadedModuleAliasAltname($mod)
      }
   }
   if {[info exists ::g_loadedModuleAutoAltname($mod)] && {autosym} in\
      $typelist} {
      if {$serialized} {
         # add a 'as|' prefix to each auto sym to distinguish them when
         # serialized
         foreach altname $::g_loadedModuleAutoAltname($mod) {
            lappend ret as|$altname
         }
      } else {
         lappend ret {*}$::g_loadedModuleAutoAltname($mod)
      }
   }

   if {$ret ne {} && $serialized} {
      # get altname info as a string that can be registered in an env var
      set ret [join [list $mod {*}$ret] [getState sub1_separator]]
   }

   return $ret
}

proc setLoadedVariant {mod args} {
   lappend ::g_loadedModuleVariant($mod) {*}$args
}

proc unsetLoadedVariant {mod} {
   if {[info exists ::g_loadedModuleVariant($mod)]} {
      unset ::g_loadedModuleVariant($mod)
   }
}

proc getLoadedVariant {mod {serialized 0}} {
   set ret {}

   set sub2sep [getState sub2_separator]
   if {[info exists ::g_loadedModuleVariant($mod)]} {
      if {$serialized} {
         # get variant info as a string that can be registered in an env var
         foreach vrspec $::g_loadedModuleVariant($mod) {
            lappend modvrspec [join $vrspec $sub2sep]
         }
         # translate range specifier ':' into '<' to distinguish from path sep
         set ret [string map {: <} [join [list $mod {*}$modvrspec]\
            [getState sub1_separator]]]
      } else {
         set ret $::g_loadedModuleVariant($mod)
      }
   }

   return $ret
}

proc getLoadedModuleWithVariantList {} {
   set modvrlist [list]
   foreach mod [getLoadedModuleList] {
      set modvr [list $mod]
      set vrlist [getVariantList $mod 5]
      if {[llength $vrlist] > 0} {
         lappend modvr {*}$vrlist
      }
      lappend modvrlist $modvr
   }
   return $modvrlist
}

proc getLoadedModuleWithVariantNotUserAskedList {} {
   set modvrlist [list]
   foreach mod [getTaggedLoadedModuleList auto-loaded] {
      set modvr [list $mod]
      set vrlist [getVariantList $mod 1]
      if {[llength $vrlist] > 0} {
         lappend modvr {*}$vrlist
      }
      lappend modvrlist $modvr
   }
   return $modvrlist
}

# register conflict violation state between loaded modules
proc setModuleConflictViolation {mod modconlist} {
   reportDebug "set conflict violation state for '$mod'"
   set ::g_conflictViolation($mod) $modconlist
   # also update violation state for loaded mod conflicting with mod
   foreach lmmod $modconlist {
      if {[appendNoDupToList ::g_conflictViolation($lmmod) $mod]} {
         reportDebug "set/update conflict violation state for '$lmmod'"
      }
   }
}

# unregister conflict violation state between modules
proc unsetModuleConflictViolation {mod} {
   if {[info exists ::g_conflictViolation($mod)]} {
      # also update violation state for loaded mod conflicting with mod
      foreach lmmod $::g_conflictViolation($mod) {
         set convio [replaceFromList\
            $::g_conflictViolation($lmmod) $mod]
         reportDebug "unset/update conflict violation state for '$lmmod'"
         if {[llength $convio] == 0} {
            unset ::g_conflictViolation($lmmod)
         } else {
            set ::g_conflictViolation($lmmod) $convio
         }
      }
      reportDebug "unset conflict violation state for '$mod'"
      unset ::g_conflictViolation($mod)
   }
}

# build dependency chain between loaded modules based on registered prereqs
proc setModuleDependency {mod} {
   set modlist [getLoadedModuleList]
   defineModEqProc [isIcase] [getConf extended_default] 1
   # only look at modules loaded prior current one to find requirements,
   # modules loaded afterward are unmet dependencies as dependents have
   # not been reloaded after them
   set modidx [lsearch -exact $modlist $mod]
   set modnpolist [lrange $modlist [expr {$modidx + 1}] end]
   set modlist [lrange $modlist 0 $modidx]
   # reverse list to get closest match if returning lastly loaded module
   if {[getConf unload_match_order] eq {returnlast}} {
      set modlist [lreverse $modlist]
   }
   set deplist {}
   set depnpolist {}

   foreach prereq [getLoadedPrereq $mod] {
      # get corresponding loaded module for each element of the prereq order
      set lmprelist {}
      set lmnpolist {}
      foreach modpre $prereq {
         set lmfound {}
         foreach lmmod $modlist {
            if {[modEq $modpre $lmmod eqstart 1 2 1]} {
               set lmfound $lmmod
               break
            }
         }

         # register an unmet dependency/requirement if no loaded mod matches
         if {$lmfound eq {}} {
            reportDebug "set an unmet requirement on '$modpre' for '$mod'"
            lappend ::g_moduleUnmetDep($mod) $modpre
            lappend ::g_unmetDepHash($modpre) $mod
         # add matching mod elsewhere
         } else {
            appendNoDupToList lmprelist $lmfound
            appendNoDupToList lmnpolist $lmfound
         }

         # look if requirement can be found in the No Particular Order list
         foreach lmmod $modnpolist {
            if {[modEq $modpre $lmmod eqstart 1 2 1]} {
               appendNoDupToList lmnpolist $lmmod
               break
            }
         }
      }

      switch -- [llength $lmprelist] {
         0 {
            # prereq not satisfied
            reportDebug "set prereq violation state for '$mod'"
            lappend ::g_prereqViolation($mod) $prereq
         }
         1 {
            set lmmod [lindex $lmprelist 0]
            lappend deplist [list $lmmod]
            # set 'is depended by' relations
            lappend ::g_dependHash($lmmod) [list $mod]
         }
         default {
            lappend deplist $lmprelist
            # many modules in prereq list, means they all set an optional dep
            foreach lmmod $lmprelist {
               lappend ::g_dependHash($lmmod) [list $mod 1]
            }
         }
      }

      # build 'is depended by' relations not taking loading order into account
      switch -- [llength $lmnpolist] {
         0 {
            # even on No Particular Order mode, prereq is not satisfied
            reportDebug "set NPO prereq violation state for '$mod'"
            lappend ::g_prereqNPOViolation($mod) $prereq
         }
         1 {
            set lmmod [lindex $lmnpolist 0]
            lappend depnpolist [list $lmmod]
            # set 'is depended by' relations
            lappend ::g_dependNPOHash($lmmod) [list $mod]
         }
         default {
            lappend depnpolist $lmnpolist
            # many modules in prereq list, means they all set an optional dep
            foreach lmmod $lmnpolist {
               lappend ::g_dependNPOHash($lmmod) [list $mod 1]
            }
         }
      }
   }

   # conflict not satisfied
   lassign [doesModuleConflict $mod] doescon modconlist
   if {$doescon} {
      setModuleConflictViolation $mod $modconlist
   }

   # update eventual registered unmet dependencies
   foreach modpre [array names ::g_unmetDepHash] {
      if {[modEq $modpre $mod eqstart 1 2 1]} {
         reportDebug "refresh requirements targeting '$modpre'"
         foreach lmmod $::g_unmetDepHash($modpre) {
            if {$mod in [getDependentLoadedModuleList [list $lmmod] 0 0]} {
               reportDebug "skip deps refresh for '$lmmod' as dep cycle\
                  detected with '$mod'"

               # remove dependency link in no particular order structs to
               # avoid cycle first in 'is depended by' struct
               if {[info exists ::g_dependNPOHash($mod)]} {
                  set depmodlist $::g_dependNPOHash($mod)
                  for {set i 0} {$i < [llength $depmodlist]} {incr i 1} {
                     if {[lindex [lindex $depmodlist $i] 0] eq $lmmod} {
                        set depmodlist [lreplace $depmodlist $i $i]
                        break
                     }
                  }
                  set ::g_dependNPOHash($mod) $depmodlist
                  reportDebug "update NPO dependent of '$mod' to\
                     '$depmodlist'"
               }
               # then update 'depend on' struct
               set lmmoddepnpolist {}
               foreach depmodlist $::g_moduleNPODepend($lmmod) {
                  if {[set depidx [lsearch -exact $depmodlist $mod]] != -1} {
                     set depmodlist [lreplace $depmodlist $depidx $depidx]
                     # implies to update consistenly alternate requirement or
                     # violation state if no alternative loaded
                     switch -- [llength $depmodlist] {
                        0 {
                           # do not know exact prereq name, so use correspond.
                           # loaded module matching it
                           lappend ::g_prereqNPOViolation($lmmod) $mod
                           reportDebug "set NPO prereq violation state for\
                              '$lmmod'"
                        }
                        1 {
                           # update alternate loaded mod which became a strong
                           # requirement
                           set altmod [lindex $depmodlist 0]
                           set ::g_dependNPOHash($altmod) [replaceFromList\
                              $::g_dependNPOHash($altmod) [list $lmmod 1]\
                              $lmmod]
                           reportDebug "update NPO dependent of '$altmod' to\
                              '$::g_dependNPOHash($altmod)'"
                        }
                     }
                  }
                  lappend lmmoddepnpolist $depmodlist
               }
               reportDebug "update NPO requirement of '$lmmod' to\
                  '$lmmoddepnpolist'"
               set ::g_moduleNPODepend($lmmod) $lmmoddepnpolist
            } else {
               # refresh actual dependencies of targeting mod
               unsetModuleDependency $lmmod
               setModuleDependency $lmmod
            }
         }
      }
   }

   # set 'depends on' relation
   reportDebug "set requirements of '$mod' to '$deplist'"
   set ::g_moduleDepend($mod) $deplist
   reportDebug "set NPO requirements of '$mod' to '$depnpolist'"
   set ::g_moduleNPODepend($mod) $depnpolist
}

# update dependency chain when unloading module
proc unsetModuleDependency {mod} {
   foreach lmmodlist $::g_moduleDepend($mod) {
      set manymod [expr {[llength $lmmodlist] > 1}]

      # unset 'is depended by' mod relations
      foreach lmmod $lmmodlist {
         if {[info exists ::g_dependHash($lmmod)]} {
            if {$manymod} {
               set hashdep [list $mod 1]
            } else {
               set hashdep [list $mod]
            }
            set ::g_dependHash($lmmod) [replaceFromList\
               $::g_dependHash($lmmod) $hashdep]
            if {[llength $::g_dependHash($lmmod)] == 0} {
               unset ::g_dependHash($lmmod)
            }
         }
      }
   }
   # unset mod's 'depends on' relation
   reportDebug "unset requirements of '$mod'"
   unset ::g_moduleDepend($mod)

   foreach lmmodlist $::g_moduleNPODepend($mod) {
      set manymod [expr {[llength $lmmodlist] > 1}]

      # unset 'is depended by' mod relations
      foreach lmmod $lmmodlist {
         if {[info exists ::g_dependNPOHash($lmmod)]} {
            if {$manymod} {
               set hashdep [list $mod 1]
            } else {
               set hashdep [list $mod]
            }
            set ::g_dependNPOHash($lmmod) [replaceFromList\
               $::g_dependNPOHash($lmmod) $hashdep]
            if {[llength $::g_dependNPOHash($lmmod)] == 0} {
               unset ::g_dependNPOHash($lmmod)
            }
         }
      }
   }
   # unset mod's No Particular Order 'depends on' relation
   reportDebug "unset NPO requirements of '$mod'"
   unset ::g_moduleNPODepend($mod)

   # unset eventual violation states
   if {[info exists ::g_prereqViolation($mod)]} {
      reportDebug "unset prereq violation state for '$mod'"
      unset ::g_prereqViolation($mod)
   }
   if {[info exists ::g_prereqNPOViolation($mod)]} {
      reportDebug "unset NPO prereq violation state for '$mod'"
      unset ::g_prereqNPOViolation($mod)
   }
   unsetModuleConflictViolation $mod

   # unset eventual registered unmet dependencies
   if {[info exists ::g_moduleUnmetDep($mod)]} {
      foreach ummod $::g_moduleUnmetDep($mod) {
         if {[info exists ::g_unmetDepHash($ummod)]} {
            set ::g_unmetDepHash($ummod) [replaceFromList\
               $::g_unmetDepHash($ummod) $mod]
            if {[llength $::g_unmetDepHash($ummod)] == 0} {
               unset ::g_unmetDepHash($ummod)
            }
         }
      }
      reportDebug "unset unmet requirements for '$mod'"
      unset ::g_moduleUnmetDep($mod)
   }

   # unset mod's 'is depended by' relations
   set hashdeplist [getDirectDependentList $mod]
   if {[llength $hashdeplist] > 0} {
      reportDebug "refresh dependent of '$mod'"
      foreach lmmod $hashdeplist {
         # refresh actual dependencies of targeting mod
         unsetModuleDependency $lmmod
         setModuleDependency $lmmod
      }
   }
}

# returns if any loaded module (if passed mod is empty) or passed mod and all
# its requirement chain satisfy their loading constraints (prereq & conflict)
proc areModuleConstraintsSatisfied {{mod {}} {nporeq 0}} {
   set ret 1
   cacheCurrentModules

   # are requirements loaded after their dependent included or not
   if {$nporeq} {
      set reqVioVar ::g_prereqNPOViolation
      set reqListVar ::g_moduleNPODepend
   } else {
      set reqVioVar ::g_prereqViolation
      set reqListVar ::g_moduleDepend
   }

   # check if any loaded module violates its prereq or conflict constraints
   if {$mod eq {}} {
      if {[array size ::g_conflictViolation] > 0 || [array size\
         $reqVioVar] > 0} {
         set ret 0
      }
   } else {
      set fulllist [list $mod]
      for {set i 0} {$i < [llength $fulllist]} {incr i 1} {
         set depmod [lindex $fulllist $i]

         # check if depmod violates its prereq or conflict constraints
         if {[info exists ::g_conflictViolation($depmod)] || [info exists\
            ${reqVioVar}($depmod)]} {
            # found violation among the requirement chain of mod so the
            # constraint of mod are not satisfied
            set ret 0
            break
         }
         # add requirements of depmod to the module to check list
         foreach lmmodlist [set ${reqListVar}($depmod)] {
            appendNoDupToList fulllist {*}$lmmodlist
         }
      }
   }

   return $ret
}

proc cacheCurrentModules {{exitonerr 1}} {
   # parse loaded modules information only once, global arrays are updated
   # afterwards when module commands update loaded modules state
   if {![isStateDefined lm_info_cached]} {
      setState lm_info_cached 1
      # mark specific as well as generic modules as loaded
      set i 0
      set modfilelist [getLoadedModuleFileList]
      set modlist [getLoadedModuleList]

      if {[llength $modlist] == [llength $modfilelist]} {
         # cache declared variant of loaded modules
         foreach modvrspec [getLoadedModuleVariantList] {
            setLoadedVariant {*}$modvrspec
            # prepare modvr designation of loaded mod needed in next steps
            set mod [lindex $modvrspec 0]
            set vrlist [getVariantList $mod 6]
            set modvr [list $mod {*}$vrlist]
            set modvrarr($mod) $modvr
         }

         # cache declared tags of loaded modules
         foreach modtag [getLoadedModuleTagList] {
            setModuleTag {*}$modtag
            # also record tag over full mod and vr designation
            set mod [lindex $modtag 0]
            if {[info exists modvrarr($mod)]} {
               setModuleTag $modvrarr($mod) {*}[lrange $modtag 1 end]
            }
         }

         # cache declared alternative names of loaded modules
         foreach modalt [getLoadedModuleAltnameList] {
            setLoadedAltname {*}$modalt
         }

         # cache declared source-sh of loaded modules
         foreach modsrcsh [getLoadedModuleSourceShList] {
            setLoadedSourceSh {*}$modsrcsh
         }

         # cache declared conflict of loaded modules
         foreach modcon [getLoadedModuleConflictList] {
            # parse module version specification to record translation
            foreach modconelt [lrange $modcon 1 end] {
               parseModuleSpecification 0 {*}$modconelt
            }
            setLoadedConflict {*}$modcon
         }

         # cache declared prereq of loaded modules, prior to setLoadedModule
         # which triggers dependency chain build
         foreach modpre [getLoadedModulePrereqList] {
            # parse module version specification to record translation
            foreach modpreeltlist [lrange $modpre 1 end] {
               foreach modpreelt $modpreeltlist {
                  parseModuleSpecification 0 {*}$modpreelt
               }
            }
            setLoadedPrereq {*}$modpre
         }

         foreach mod $modlist {
            # get all tags also recorded on mod and vr designation
            if {[info exists modvrarr($mod)]} {
               set modvr $modvrarr($mod)
            } else {
               set modvr {}
            }
            setLoadedModule $mod [lindex $modfilelist $i] [expr\
               {![isModuleTagged $mod auto-loaded 1]}] $modvr
            incr i
         }

         reportDebug "$i loaded"
      } else {
         set errproc [expr {$exitonerr ? {reportErrorAndExit} :\
            {reportError}}]
         $errproc "Loaded environment state is\
            inconsistent\nLOADEDMODULES=$modlist\n_LMFILES_=$modfilelist"
      }
   }
}

# This proc resolves module aliases or version aliases to the real module name
# and version.
proc resolveModuleVersionOrAlias {name icase} {
   set name [getArrayKey ::g_moduleResolved $name $icase]
   if {[info exists ::g_moduleResolved($name)]} {
      set ret $::g_moduleResolved($name)
   } else {
      set ret $name
   }

   reportTrace "'$name' into '$ret'" Resolve
   return $ret
}

proc parseAccessIssue {modfile} {
   # retrieve and return access issue message
   if {[regexp {POSIX .* \{(.*)\}$} $::errorCode match errMsg]} {
      return "[string totitle $errMsg] on '$modfile'"
   } else {
      return "Cannot access '$modfile'"
   }
}

proc checkValidModule {modfile} {
   # test file only once, cache result obtained to minimize file query
   return [expr {[info exists ::g_modfileValid($modfile)]\
      ? $::g_modfileValid($modfile)\
      : [set ::g_modfileValid($modfile) [readModuleContent $modfile 1 1]]}]
}

# get file modification time, cache it at first query, use cache afterward
proc getFileMtime {fpath} {
   if {[info exists ::g_fileMtime($fpath)]} {
      return $::g_fileMtime($fpath)
   } else {
      return [set ::g_fileMtime($fpath) [file mtime $fpath]]
   }
}

# define proc that will be used as fallback to command provided by extension
# library in case this library is not loaded
proc __readFile {filename {firstline 0} {must_have_cookie 0}} {
   set fid [open $filename r]
   if {$firstline} {
      set fdata [gets $fid]
   } else {
      # read a first data chunk and check if magic cookie is there if
      # mandatory. skip read of any additional content if mandatory cookie is
      # not found
      set fdata [read $fid 4096]
      if {![eof $fid] && (!$must_have_cookie || [string equal -length 8\
         $fdata {#%Module}])} {
         append fdata [read $fid]
      }
   }
   close $fid
   return $fdata
}

proc readModuleContent {modfile {report_read_issue 0} {only_check_validity\
   0}} {
   set res {}

   # read file
   if {[catch {
      if {[info exists ::g_modfileContent($modfile)]} {
         lassign $::g_modfileContent($modfile) fh fdata
      } else {
         # only read beginning of file if just checking validity and not
         # asked to always fully read files. when full file should be read
         # cookie is checked if asked to avoid to fully read non modfile
         set fdata [readFile $modfile [expr {$only_check_validity &&\
            ![currentState always_read_full_file]}] 1]
         # extract magic cookie (first word of modulefile)
         set fh [string trimright [lindex [split [string range $fdata 0 32]]\
            0] #]
         # cache full file read to minimize file operations
         if {!$only_check_validity || [currentState always_read_full_file]} {
            set ::g_modfileContent($modfile) [list $fh $fdata]
         }
      }
   } errMsg ]} {
      if {$report_read_issue} {
         set msg [parseAccessIssue $modfile]
         if {$only_check_validity} {
            set res [list accesserr $msg]
         } else {
            reportError $msg
         }
      }
   } else {
      # check module validity
      if {![string equal -length 8 $fh {#%Module}]} {
         set msg {Magic cookie '#%Module' missing}
         if {$only_check_validity} {
            set res [list invalid $msg]
         } else {
            reportInternalBug $msg $modfile
         }
      # check if min version requirement is met
      } elseif {[string length $fh] > 8 && [getConf mcookie_version_check] &&\
         [versioncmp {@MODULES_RELEASE@} [string range $fh 8 end]] <0} {
         set msg "Modulefile requires at least Modules version [string range\
            $fh 8 end]"
         if {$only_check_validity} {
            set res [list invalid $msg]
         } else {
            reportInternalBug $msg $modfile
         }
      } else {
         if {$only_check_validity} {
            # set validity information as result
            set res [list true {}]
         } else {
            # set file content as result
            set res $fdata
         }
      }
   }

   return $res
}

# If given module maps to default or other symbolic versions, a list of
# those versions is returned. This takes module/version as an argument.
proc getVersAliasList {mod} {
   set sym_list {}
   if {[info exists ::g_symbolHash($mod)]} {
      set sym_list $::g_symbolHash($mod)
      # withdraw hidden symbol from list
      if {[info exists ::g_hiddenSymHash($mod)]} {
         lassign [getDiffBetweenList $sym_list $::g_hiddenSymHash($mod)]\
            sym_list
      }
   }

   reportDebug "'$mod' has symbolic version list '$sym_list'"
   return $sym_list
}

proc doesModuleHaveSym {mod} {
   # is there any non-hidden symbol for mod
   return [expr {[info exists ::g_symbolHash($mod)] && (![info exists\
      ::g_hiddenSymHash($mod)] || [llength [lindex [getDiffBetweenList\
      $::g_symbolHash($mod) $::g_hiddenSymHash($mod)] 0]] > 0)}]
}

# get list of elements located in a directory passed as argument. a flag is
# set after each element to know if it is considered hidden or not. a
# fetch_dotversion argument controls whether .version file should be looked at
# in directory .proc will be used as a fallback to command provided by
# extension library
proc __getFilesInDirectory {dir fetch_dotversion} {
   set dir_list [list]

   set elt_list [glob -nocomplain $dir/*]

   # Add each element in the current directory to the list
   foreach elt $elt_list {
      lappend dir_list $elt 0
   }

   # search for hidden files
   foreach elt [glob -nocomplain -types hidden -directory $dir -tails *] {
      switch -- $elt {
         . - .. { }
         .modulerc - .version {
            if {($fetch_dotversion || $elt ne {.version}) && [file readable\
               $dir/$elt]} {
               lappend dir_list $dir/$elt 0
            }
         }
         default {
            lappend dir_list $dir/$elt 1
         }
      }
   }

   return $dir_list
}

# check if an existing findModules cache entry matches current search by
# evaluating search ids. if an exact match cannot be found, look at saved
# searches that contains current search (superset of looked elements), extra
# elements will be filtered-out by GetModules
proc findModulesInMemCache {searchid} {
   # exact same search is cached
   if {[info exists ::g_foundModulesMemCache($searchid)]} {
      set match_searchid $searchid
      set mod_list $::g_foundModulesMemCache($searchid)
   # look for a superset search
   } else {
      set match_searchid {}
      set mod_list {}
      foreach cacheid [array names ::g_foundModulesMemCache] {
         # cache id acts as pattern to check if it contains current search
         if {[string match $cacheid $searchid]} {
            set match_searchid $cacheid
            set mod_list $::g_foundModulesMemCache($cacheid)
            break
         }
      }
   }

   return [list $match_searchid $mod_list]
}

# finds all module-related files matching mod in the module path dir
proc findModules {dir mod depthlvl fetch_mtime} {
   reportDebug "finding '$mod' in $dir (depthlvl=$depthlvl,\
      fetch_mtime=$fetch_mtime)"

   # generated search id (for cache search/save) by compacting given args
   set searchid $dir:$mod:$depthlvl:$fetch_mtime

   # look at memory cache for a compatible result
   lassign [findModulesInMemCache $searchid] cache_searchid cache_list
   if {$cache_searchid ne {}} {
      reportDebug "use cache entry '$cache_searchid'"
      return $cache_list
   }

   defineModEqStaticProc [isIcase] [getConf extended_default] $mod

   # every entries are requested
   set findall [expr {$mod eq {} || $mod eq {*}}]

   # use catch protection to handle non-readable and non-existent dir
   if {[catch {
      set full_list {}
      foreach {fpelt hid} [getFilesInDirectory $dir 0] {
         set elt [file tail $fpelt]
         # include any .modulerc file found at the modulepath root
         if {$elt eq {.modulerc} || $findall || [modEqStatic $elt match]} {
            lappend full_list $fpelt
         }
      }
   }]} {
      return {}
   }

   foreach igndir [getConf ignored_dirs] {
      set ignored_dirs($igndir) 1
   }
   array set mod_list {}
   for {set i 0} {$i < [llength $full_list]} {incr i 1} {
      set element [lindex $full_list $i]
      set tail [file tail $element]
      set modulename [getModuleNameFromModulepath $element $dir]
      set parentname [file dirname $modulename]
      set moddepthlvl [llength [file split $modulename]]
      if {[file isdirectory $element]} {
         if {![info exists ignored_dirs($tail)]} {
            if {[catch {
               set elt_list [getFilesInDirectory $element 1]
            } errMsg]} {
               set mod_list($modulename) [list accesserr [parseAccessIssue\
                  $element] $element]
            } else {
               # Add each element in the current directory to the list
               foreach {fpelt hid} $elt_list {
                  lappend full_list $fpelt
                  # Flag hidden files
                  if {$hid} {
                     set hidden_list($fpelt) 1
                  }
               }
            }
         }
      } else {
         switch -glob -- $tail {
            .modulerc {
               set mod_list($modulename) [list modulerc]
            }
            .version {
               # skip .version file from different depth level than search
               # targets if no in depth mode is enabled
               if {$depthlvl == 0 || $moddepthlvl == $depthlvl} {
                  set mod_list($modulename) [list modulerc]
               }
            }
            *~ - *,v - \#*\# { }
            default {
               # skip modfile in no in depth mode search if it does not relate
               # to targeted depth level and one valid modfile has already be
               # found for the dirs lying at other depth level
               if {$depthlvl == 0 || $moddepthlvl == $depthlvl || ![info\
                  exists modfile_indir($parentname)]} {
                  lassign [checkValidModule $element] check_valid check_msg
                  switch -- $check_valid {
                     true {
                        set mtime [expr {$fetch_mtime ? [getFileMtime\
                           $element] : {}}]
                        set mod_list($modulename) [list modulefile $mtime\
                           $element]

                        # a valid modfile has been found in directory
                        if {![info exists hidden_list($element)]} {
                           set modfile_indir($parentname) 1
                        }
                     }
                     default {
                        # register check error and relative message to get it
                        # in case of direct access of this module element, but
                        # no registering in parent directory structure as
                        # element is not valid
                        set mod_list($modulename) [list $check_valid\
                           $check_msg $element]
                     }
                  }
               }
            }
         }
      }
   }

   reportDebug "found [array names mod_list]"

   # cache search results
   reportDebug "create cache entry '$searchid'"
   set found_list [array get mod_list]
   set ::g_foundModulesMemCache($searchid) $found_list

   return $found_list
}

proc getModules {dir {mod {}} {fetch_mtime 0} {search {}} {filter {}}} {
   global g_sourceAlias g_sourceVersion g_sourceVirtual g_rcAlias\
      g_moduleAlias g_rcVersion g_moduleVersion g_rcVirtual g_moduleVirtual\
      g_rcfilesSourced

   reportDebug "get '$mod' in $dir (fetch_mtime=$fetch_mtime, search=$search,\
      filter=$filter)"

   # generated search id (for cache search/save) by compacting given args
   set searchid $dir:$mod:$fetch_mtime:$search:$filter

   # look at memory cache for a compatible result
   if {[info exists ::g_gotModulesMemCache($searchid)]} {
      reportDebug "use cache entry '$searchid'"
      return $::g_gotModulesMemCache($searchid)
   }

   # extract one module name from query
   set modqe [getOneModuleFromVersSpec $mod]

   # perform an in depth search or not
   set indepth [expr {{noindepth} ni $search}]

   # set a default if none defined on directory entries
   set implicitdfl [getConf implicit_default]

   # automatically define latest and default sym for all modules
   set autosymbol [expr {$implicitdfl && [getConf advanced_version_spec]}]

   # match passed name against any part of avail module names
   set contains [expr {{contains} in $search}]
   set mtest [expr {$contains ? {matchin} : {match}}]

   set icase [isIcase]

   set wild [expr {{wild} in $search}]

   # will only keep default or latest elts in the end or remove plain dirs
   set filtering [expr {$filter eq {noplaindir}}]
   set keeping [expr {!$filtering && $filter ne {}}]

   # check search query string corresponds to directory
   set querydir [string trimright $modqe *]
   set isquerydir [expr {[string index $querydir end] eq {/}}]
   set querydir [string trimright $querydir /]
   set querydepth [countChar $modqe /]

   # get directory relative to module name
   set moddir [getModuleNameFromVersSpec $mod]
   set hasmoddir [expr {$moddir ne {.}}]
   set modroot [getModuleRootFromVersSpec $mod]

   set earlyfilter [expr {!$contains && !$wild && $modroot eq [string\
      map {* {} ? {}} $modroot]}]

   # are result entries gathered in a resolution context ?
   set resctx [expr {{resolve} in $search}]

   # if search for global or user rc alias only, no dir lookup is performed
   # and aliases from g_rcAlias are returned
   if {{rc_alias_only} in $search} {
      set add_rc_defs 1
      array set found_list {}
   } else {
      # find modules by searching mod root name in order to catch all module
      # related entries to correctly computed auto symbols afterward

      if {$contains} {
         set findmod *
      } else {
         set findmod $modroot
         # if searched mod is an empty or flat element append wildcard
         # character to match anything starting with mod
         if {$wild && !$hasmoddir && [string index $findmod end] ne {*}} {
            append findmod *
         }
      }

      # add alias/version definitions from global or user rc to result
      set add_rc_defs [expr {{rc_defs_included} in $search}]

      # if no indepth mode search, pass the depth level of the search query
      set depthlvl [expr {$indepth ? 0 : [expr {$querydepth + 1}]}]

      array set found_list [findModules $dir $findmod $depthlvl $fetch_mtime]
   }

   # Phase #1: consolidate every kind of entries (directory, modulefile,
   # symbolic version, alias and virtual module) in found_list

   array set err_list {}
   array set versmod_list {}

   foreach elt [lsort [array names found_list]] {
      switch -- [lindex $found_list($elt) 0] {
         modulerc {
            # process rc files them remove them from found_list
            if {![info exists g_rcfilesSourced($dir/$elt)]} {
               execute-modulerc $dir/$elt $elt $elt
               # Keep track of already sourced rc files not to run them again
               set g_rcfilesSourced($dir/$elt) 1
            }
            unset found_list($elt)
         }
         modulefile {
         }
         default {
            # flag entries with error
            set err_list($elt) 1
         }
      }
   }

   # add all versions found when parsing .version or .modulerc files in this
   # directory or in global or user rc definitions
   foreach vers [array names g_moduleVersion] {
      set versmod $g_moduleVersion($vers)
      if {($dir ne {} && [string first $dir/ $g_sourceVersion($vers)] == 0)\
         || [info exists g_rcVersion($vers)]} {
         set found_list($vers) [list version $versmod]

         # build module symbol list
         lappend versmod_list($versmod) $vers
      # add global/user rc def to module symbol list in any cases
      } elseif {!$add_rc_defs && [info exists g_rcVersion($vers)]} {
         lappend versmod_list($versmod) $vers
      }
   }

   # add aliases found when parsing .version or .modulerc files in this
   # directory (skip aliases not registered from this directory except if
   # global or user rc definitions should be included)
   foreach alias [array names g_moduleAlias] {
      if {($dir ne {} && [string first $dir/ $g_sourceAlias($alias)] == 0)\
         || ($add_rc_defs && [info exists g_rcAlias($alias)])} {
         set found_list($alias) [list alias $g_moduleAlias($alias)]
      }
   }

   # add virtual mods found when parsing .version or .modulerc files in this
   # directory (skip virtual mods not registered from this directory except if
   # global or user rc definitions should be included)
   foreach virt [array names g_moduleVirtual] {
      if {($dir ne {} && [string first $dir/ $g_sourceVirtual($virt)] == 0)\
         || ($add_rc_defs && [info exists g_rcVirtual($virt)])} {
         lassign [checkValidModule $g_moduleVirtual($virt)] check_valid\
            check_msg
         switch -- $check_valid {
            true {
               set mtime [expr {$fetch_mtime ? [getFileMtime\
                  $g_moduleVirtual($virt)] : {}}]
               # set mtime at index 1 like a modulefile entry
               set found_list($virt) [list virtual $mtime\
                  $g_moduleVirtual($virt)]
            }
            default {
               # register check error and relative message to get it in
               # case of direct access of this module element
               set found_list($virt) [list $check_valid $check_msg\
                  $g_moduleVirtual($virt)]
               set err_list($virt) 1
            }
         }
      }
   }

   # Phase #2: filter-out dynamically hidden or expired elements

   # define module name and version comparison procs
   defineModStartNbProc $icase
   defineModEqProc $icase [getConf extended_default]
   defineModEqStaticProc $icase [getConf extended_default] $mod

   # remove hidden elements unless they are (or their symbols) targeted by
   # search query.
   foreach elt [array names found_list] {
      if {$earlyfilter && ![modEq $modroot $elt eqstart]} {
         unset found_list($elt)
         continue
      }
      if {[lassign [isModuleHidden $elt $mod 1] hidlvl hidmatch hidbydef]} {
         # is there a symbol that matches query (bare module name query
         # matches default symbol on resolve context or if onlydefaults filter
         # is applied)
         if {!$hidmatch && [info exists versmod_list($elt)]} {
            foreach eltsym $versmod_list($elt) {
               if {[modEqStatic $eltsym] || (($resctx || $filter eq\
                  {onlydefaults}) && "$mod/default" eq $eltsym)} {
                  set hidmatch 1
                  break
               }
            }
         }
         # consider 'default' symbols are explicitly specified if
         # onlydefaults filter applied or resolving bare module name
         if {!$hidmatch && [lindex $found_list($elt) 0] eq {version} && [file\
            tail $elt] eq {default} && ($filter eq {onlydefaults} || ($resctx\
            && "$mod/default" eq $elt))} {
            set hidmatch 1
         }

         # not hidden if matched unless if hard hiding apply
         if {!$hidmatch || $hidlvl > 1} {
            # record hidden symbol, not to display it in listModules
            if {[lindex $found_list($elt) 0] eq {version}} {
               lappend ::g_hiddenSymHash([lindex $found_list($elt) 1]) [file\
                  tail $elt]
            }
            # transform forbidden module in error entry if it specifically
            # matches search query
            if {$hidlvl == 2 && $hidmatch && [isModuleTagged $elt\
               forbidden]} {
               set found_list($elt) [list accesserr [getForbiddenMsg $elt]]
               set err_list($elt) 1
            } else {
               unset found_list($elt)
            }
         }
      }
      # apply hidden tag if an hidden definition apply to module
      if {$hidbydef} {
         setModuleTag $elt hidden
      }
   }

   # Phase #3: elaborate directory content with default element selection

   array set dir_list {}
   array set autosym_list {}

   # build list of elements contained in each directory
   foreach elt [array names found_list] {
      # add a ref to element in its parent directory unless element has error
      # or is a symbolic version then recursively add parent element until
      # reaching top directory
      if {![info exists err_list($elt)] && [lindex $found_list($elt) 0] ne\
         {version}} {
         set direlt $elt
         while {[set pardir [file dirname $direlt]] ne {.}} {
            appendNoDupToList dir_list($pardir) [file tail $direlt]
            set direlt $pardir
         }
      }
   }

   # determine default element for each directory and record sorted elt list
   # unless if an alias or a virtual module has overwritten directory entry
   foreach elt [array names dir_list] {
      if {![info exists found_list($elt)]} {
         set dir_list($elt) [lsort -dictionary $dir_list($elt)]
         # get default element: explicitly defined default (whether it exists
         # or is in error) or implicit default if enabled
         if {[info exists found_list($elt/default)] && [lindex\
            $found_list($elt/default) 0] eq {version}} {
            set dfl [file tail [lindex $found_list($elt/default) 1]]
         } elseif {$implicitdfl} {
            set dfl [lindex $dir_list($elt) end]
         } else {
            set dfl {}
         }
         # record directory properties
         set found_list($elt) [list directory $dfl {*}$dir_list($elt)]

         # automatically define symbols for all modules matching query if
         # these names do not exist yet or if in error, in which case only
         # auto symbol resolution is set
         if {$autosymbol && (!$hasmoddir || [modEq $modroot $elt eqstart])} {
            if {![info exists found_list($elt/default)] || [info exists\
               err_list($elt/default)]} {
               if {![info exists found_list($elt/default)]} {
                  set found_list($elt/default) [list version $elt/$dfl]
                  set autosym_list($elt/default) 1
               }
               setModuleResolution $elt/default $elt/$dfl default 1 1
            }
            if {![info exists found_list($elt/latest)] || [info exists\
               err_list($elt/latest)]} {
               set lat [lindex $dir_list($elt) end]
               if {![info exists found_list($elt/latest)]} {
                  set found_list($elt/latest) [list version $elt/$lat]
                  set autosym_list($elt/latest) 1
               }
               setModuleResolution $elt/latest $elt/$lat latest 1 1
            }
         }
      }
   }

   # Phase #4: filter results to keep those matching search query

   # define module name and version comparison procs
   defineDoesModMatchAtDepthProc $indepth $querydepth $mtest

   # element to include in output
   set report_dirwsym [isEltInReport dirwsym]

   array set mod_list {}
   array set fdir_list {}
   array set keep_list {}

   # keep element matching query, add directory of element matching query,
   # also add directory to result if query name finishes with trailing slash;
   # only keep auto syms if fully matched or version not specified in query;
   # (hidden elements have been filtered on phase 2)
   foreach elt [array names found_list] {
      set elt_type [lindex $found_list($elt) 0]
      if {(($wild && [doesModMatchAtDepth $elt]) || (!$wild && ([modEqStatic\
         $elt match /*] || [modEqStatic $elt match] || ($hasmoddir &&\
         $elt_type eq {directory} && [modEq $moddir $elt]))) || ($isquerydir\
         && $elt_type eq {directory} && [modEq $querydir $elt match 0])) &&\
         (![info exists autosym_list($elt)] || ([countChar $elt /]\
         != $querydepth && !$contains) || [modEqStatic $elt]) && ![info\
         exists mod_list($elt)]} {
         if {$elt_type eq {directory}} {
            # add matching directory to the result list, its entries will be
            # computed in a second time and directory will be dropped if it
            # has no entry in the end
            set mod_list($elt) [list directory]
            # add dir to the filter dir list to enable its removal in next
            # step if dir is empty
            if {![info exists fdir_list($elt)]} {
               set fdir_list($elt) {}
            }
         } else {
            set mod_list($elt) $found_list($elt)
         }

         # version may matches query but not its target, so it should be in
         # this case manually added to result (if it exists)
         if {$elt_type eq {version}} {
            # resolve eventual icase target
            set versmod [getArrayKey found_list [lindex $mod_list($elt) 1]\
               $icase]
            # add target to dir struct (not version) if not already recorded
            set direlt .

            # recursively add targets to result (process directory content if
            # target is a directory
            set tgt_list [list $versmod]
            for {set i 0} {$i < [llength $tgt_list]} {incr i} {
               set tgt [lindex $tgt_list $i]

               if {![info exists mod_list($tgt)]} {
                  if {[info exists found_list($tgt)]} {
                     set mod_list($tgt) $found_list($tgt)
                     # version target is directory: recursively add content
                     if {[lindex $mod_list($tgt) 0] eq {directory}} {
                        foreach tgtelt $dir_list($tgt) {
                           lappend tgt_list $tgt/$tgtelt
                        }
                        # add dir to the filter dir list to enable its removal
                        # in next step if dir is empty
                        if {![info exists fdir_list($tgt)]} {
                           set fdir_list($tgt) {}
                        }
                     }
                  }

                  # record target in dir struct if part of found elts or if
                  # hidden but should not be in error
                  set pardir [file dirname $tgt]
                  if {([info exists found_list($tgt)] || ($pardir ne {.} &&\
                     ![info exists found_list($pardir)] && [isModuleHidden\
                     $tgt $mod])) && ![info exists err_list($tgt)]} {
                     # create parent directory if it does not exist
                     if {$pardir ne {.} && ![info exists\
                        found_list($pardir)]} {
                        set found_list($pardir) [list directory]
                        set mod_list($pardir) [list directory]
                     }

                     if {$i == 0} {
                        set direlt $tgt
                     } else {
                        lappend fdir_list([file dirname $tgt]) [file tail\
                           $tgt]
                     }
                  }
               }
            }
         # skip adding element to directory content if in error
         } elseif {[info exists err_list($elt)]} {
            set direlt .
         } else {
            set direlt $elt
         }

         # track directory content, as directory are also reported to their
         # parent directory the directory structure is also tracked
         if {[set pardir [file dirname $direlt]] ne {.}} {
            lappend fdir_list($pardir) [file tail $direlt]
         # track top level entries that will be kept if result is filtered
         } elseif {$keeping && $direlt ne {.} && $elt_type ne {directory}} {
            set keep_list($elt) 1
         }
      }
   }

   # determine default element for each directory and record sorted element
   # list unless directory entry has been overwritten by a different module
   # kind or unless only matching directory should be part of result
   foreach elt [lsort -decreasing [array names fdir_list]] {
      if {[lindex $found_list($elt) 0] eq {directory} && ([info exists\
         mod_list($elt)] || $keeping)} {
         set fdir_list($elt) [lsort -dictionary $fdir_list($elt)]
         # get default element: explicitly defined default if included in
         # result or not found or implicit default if enabled
         if {[info exists found_list($elt/default)] && [lindex\
            $found_list($elt/default) 0] eq {version} && ([info exists\
            mod_list([set versmod [lindex $found_list($elt/default) 1]])] ||\
            ![info exists found_list($versmod)])} {
            set dfl [file tail $versmod]
         } elseif {$implicitdfl} {
            set dfl [lindex $fdir_list($elt) end]
         } else {
            set dfl {}
         }
         # remove empty dirs
         if {[llength $fdir_list($elt)] == 0} {
            unset mod_list($elt)
            unset fdir_list($elt)
            # remove unset dir reference in parent directory. parent dir
            # will be treated after unset dir (due to decreasing sort) if it
            # needs to get in turn unset
            if {[set pardir [file dirname $elt]] ne {.}} {
               set fdir_list($pardir) [replaceFromList $fdir_list($pardir)\
                  [file tail $elt]]
            }
         } else {
            # record directory properties
            set mod_list($elt) [list directory $dfl {*}$fdir_list($elt)]

            # list elements to keep for filtering step
            if {$keeping} {
               if {$filter eq {onlylatest}} {
                  set keepelt $elt/[lindex $fdir_list($elt) end]
               } elseif {$dfl ne {}} {
                  set keepelt $elt/$dfl
               } else {
                  set keepelt {}
               }

               # keep directory if its element depth is deeper than query
               if {!$indepth && [countChar $keepelt /] > $querydepth} {
                  set keep_list($elt) 1
               # otherwise only keep existing modules (not directories)
               } elseif {[info exists mod_list($keepelt)] && [lindex\
                  $mod_list($keepelt) 0] ne {directory}} {
                  set keep_list($keepelt) 1
               }
            # when noplaindir filtering, only keep dirs with syms when indepth
            # enabled or if corresponds to query depth when indepth disabled
            } elseif {$filtering && $filter eq {noplaindir} &&\
               (($indepth && (!$report_dirwsym || ![doesModuleHaveSym $elt]))\
               || (!$indepth && [countChar $elt /] != $querydepth))} {
               unset mod_list($elt)
            }
         }
      }
   }

   # now all matching modulefiles are settled, only keep those found at search
   # query depth level if 'noindepth' mode asked
   if {!$indepth} {
      # remove entries with more filename path separator than query pattern
      foreach elt [array names mod_list] {
         if {[countChar $elt /] > $querydepth} {
            unset mod_list($elt)
         }
      }
   }

   # if result should be filtered, only keep marked elements
   if {$keeping} {
      foreach elt [array names mod_list] {
         if {![info exists keep_list($elt)]} {
            unset mod_list($elt)
         }
      }
   }

   # Phase #5: consolidate tags set for retained modules

   if {[currentState commandname] eq {avail}} {
      # load tags from loaded modules in the global structure prior collecting
      # tags found during this getModules evaluation
      cacheCurrentModules 0

      foreach elt [array names mod_list] {
         switch -- [lindex $mod_list($elt) 0] {
            alias - modulefile - virtual {
               # gather all tags applying to elt
               collectModuleTags $elt
            }
         }
      }
   }

   reportTrace "{[array names mod_list]} matching '$mod' in '$dir'" {Get\
      modules}

   # cache search results
   reportDebug "create cache entry '$searchid'"
   set got_list [array get mod_list]
   set ::g_gotModulesMemCache($searchid) $got_list

   return $got_list
}

# ;;; Local Variables: ***
# ;;; mode:tcl ***
# ;;; End: ***
# vim:set tabstop=3 shiftwidth=3 expandtab autoindent:
